fix(analytics): where 门拒收 $ 算子与非 $ 键混写的字段约束(#6444) - #6583
Merged
Conversation
…sibling keys (#6444) A field constraint object carrying $-operator keys and non-$ keys at once used to compile its operators and silently DROP every non-$ sibling — fieldLeaves's operator arm iterated opKeys only and returned, and the nested-relation flatten sits after that early return. Dropping a conjunct WIDENS the query (#3650), and inside a $not the surviving guard could be contradictory, negating to TRUE — every row. Ruled Option A (refuse) on 2026-08-08: the mixed wrapper is refused through the module's one envelope (INVALID_FILTER / 400), with a message that names the offending non-$ key(s) and shows BOTH legal rewrites — the operator spelling (gte -> $gte) and the nested-relation form — because the shape has two intents this door cannot tell apart. Option B (flattening) was rejected: it would compile the missing-$ typo into a predicate on a non-existent member such as amount.gte. The two pure shapes do not move: all-$ wrappers compile as before, all-non-$ wrappers keep flattening to the dotted member. #6386's sibling-drop pin flips to a positive refusal assertion; the refusal ledger gains the eleventh row (addedAfter5352: #6444). read-scope-sql.ts, the $null/$exists flag semantics and the null-comparand rulings (#5332 / #5526) are untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01USNUyHEr7uaU6MoEWXitei
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-project-manager
marked this pull request as ready for review
August 8, 2026 05:56
os-project-manager
enabled auto-merge
August 8, 2026 05:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #6444
按 2026-08-08 02:12Z 维护者裁决(Option A——拒收)实施,基于已并入 #6445 的
origin/main(1a53a0253)。只动strategies/filter-normalizer.ts与其测试;同包另一扇门read-scope-sql.ts零字节未动(不在 diff 中)。未实现任何展平回退(Option B 被裁决否决)。实测(先验前提,再实现)
在
origin/main@1a53a0253上直接调normalizeAnalyticsFilterTree({ where })逐行复测。前提成立:opKeys.length > 0的分支只遍历$键就return,嵌套关系展平在其后,走不到——非$兄弟键静默消失,与值无关:where{d: {$eq: 1, nested: 'x'}}d equals [1]nested合取项静默消失{d: {$eq: 1, nested: undefined}}d equals [1]{amount: {gte: 10, $lte: 20}}amount lte 20$的典型手误:下界静默没了{$not: {d: {$eq: 1, nested: 'x'}}}NOT(d set AND d = 1){$not: {d: {$null: true, nested: 'x'}}}NOT(d set AND d notSet)最后一行是本轮实测新量到的、比 issue 正文更响的一格:#5146 的守卫在兄弟键还在时判的
requireValue,兄弟键随后在fieldLeaves里消失,剩下的合取自相矛盾,取反后放行整个数据集。每一行方向都是加宽——同一个函数MONGO_TO_CUBE_OP未命中分支的注释明令禁止的 #3650 败法。裁决四项要求——逐项合规
$键并给出两种合法改写(算子拼法gte→$gte和嵌套关系形),区分两种无法消歧的意图mixedFieldWrapperError:逐键点名(多键全列,有专门用例)、"gte" → "$gte"加原位示例、嵌套形加它编成的点号成员"amount.gte"加显式$and组合(一个 JSON 对象写不了两次同名键)。消息合同由shows BOTH legal rewrites用例逐片断言;措辞唯一性(#5240)由擦除比对用例钉死code+status(ADR-0112),裸toThrow()不算code === 'INVALID_FILTER'且status === 400;账本的 envelope 块对第 11 行自动覆盖同两项a non-$ SIBLING … still dropped)翻成正向拒收断言filter-normalizer-undefined-comparand.test.ts同一用例原位翻转,保留原 pin 的同两条输入(nested: undefined/nested: 'x'),断言消息 + code + status;注释记下翻转前后的可读历史read-scope-sql.ts零字节#6410 编译面清单——逐面申报
filter-normalizer.tsassertUnmixedFieldWrapper于fieldLeaves包装分支、#5240 空约束闸之后read-scope-sql.tscompileField:keys.some((k) => !k.startsWith('$'))即 fail-closed 拒收(:379-382)。零字节未动sql-driver.ts(wasm / turso-local 继承)$兄弟键落到发射器default: throw unsupportedFilterError("Unsupported filter operator …")(:7416-7422)——响亮拒收、点名键,不丢buildWhereSQLunsupportedOperator对非$键有专门措辞(object comparand whose key "…" is not an operator,:2189-2196),invalidFilterError信封matchesFilterConditionevalField:任何非$键使整个约束return false(matches-filter.ts:161)——静默收窄(该行被排除),既不丢键加宽也不响亮拒收。与裁决面向不一致但方向相反且无声;按裁决要求 4 与 #5930 hold 不在本单动,差异如实记档having-filter.ts(半面,#5905)isOperatorObject只要有$键即真,随后全部键按算子迭代,非$键落default: throw unknownOperator(op, 'condition')(:194-196)反向验证(方向先预判、写进测试文件头,再跑)
唯一旋钮:摘掉
fieldLeaves里assertUnmixedFieldWrapper(key, wrapper)这一次调用。预判:新文件 19 条中 15 红(8 个位置行 + 多键行 + 措辞唯一性行 + 双改写行 + 4 条
$not改写路径行),4 条对照绿(两条纯形状、邻座拒收措辞、$eq: undefined混写行——它由 #6386 的闸先拒,与本闸无关);翻转的 pin 1 红;账本第 11 行在两个循环里 2 红;其余全绿。合计 18 红。实测:
Test Files 3 failed | 66 passed / Tests 18 failed | 1393 passed——红的正是预判的 18 条、只在预判的 3 个文件里,逐条名单核对一致;两个纯形状对照块、#6386 全部用例、null对照组全程未红一行。恢复闸后全绿。措辞唯一性用例带着 #6445 在孪生用例上量出的防空转守卫(先逐行断言确实拒收,再擦除比对),反向档位下它按预期红在「did not refuse」一步,而不是空着绿。
null对照组与 #6386 的闸:零字节filter-normalizer-undefined-comparand.test.ts的 diff 只有翻转的那一个用例:13 行NULL_CONTROL、七行表、位置清单、$null/$exists旗标块逐字节未动,全程绿。comparand()、$null/$exists恒等读、#5332/#5526 裁决——不在 diff 中。两闸次序为实测事实并钉了一条用例:{d: {$eq: undefined, nested: 'x'}}由 #6386 的闸先答(同信封,REST 面无差别),注释写明这是实现事实而非契约。夹具分诊与消费半径
where门把undefined值的键整个丢掉 —— 单键 where 退化成「无过滤器」,方向是加宽(#6125 五面表漏记的第六、七种读法) #6386 的 sibling-drop pin,原位翻正,同输入。filter-refusal-envelope.test.ts的covers every refusing site in the module是全量声明,新增第 11 行(addedAfter5352: '#6444',尊重 fix(analytics):where门的undefined比较数改为拒收,不再把整个键丢掉(#6386) #6445 立下的历史/全量双职责区分——不混入issueBullet: false集合),计数 10 → 11。packages/rest/analytics-filter-refusal-envelope.test.ts)是采样表而非全量账本(fix(analytics):where门的undefined比较数改为拒收,不再把整个键丢掉(#6386) #6445 同样未加行),不动;信封经由通用读取跨 seam,下游全绿见下。normalizeAnalyticsFilterTree调用方 3 个文件全在本包;examples / plugin-reports 的库存 metadata 过滤器全文扫过——全部纯$包装或标量等值,无一处混写形状;下游rest、runtime全量跑过。门禁(全部实跑)
pnpm --filter @objectstack/service-analytics testTest Files 69 passed (69) / Tests 1411 passed (1411)(基线 68 文件;本 PR +1 文件 +19 用例,另 3 条用例原位改写)tsc --noEmit -p packages/services/service-analyticscheck-type-check-coverage.mjs账本记录的 10 一致;逐文件核对,全部在原有 3 个测试文件,本 PR 改动文件零类型错误eslint(4 个改动 ts 文件)node scripts/check-nul-bytes.mjsOK (scanned 6158 tracked text file(s));另对 5 个改动文件跑grep -naP控制字节自扫描,cleanpnpm check:engine-double-contractOK — 92 pinned, 133 in the DEBT ledger, 2 exempt.check:error-code-casing✓ no lowercase error codes in 3198 scanned file(s) (ADR-0112).check:route-envelope✓ Dispatcher domains — 16 audited … (0 ratcheted)check:empty-changeset✓ … (1 declaring changeset(s) added)pnpm --filter @objectstack/rest testTest Files 66 passed (66) / Tests 939 passed (939)pnpm --filter @objectstack/runtime testTest Files 111 passed (111) / Tests 1611 passed (1611)Changeset:
.changeset/mixed-wrapper-refusal.md,patch @@objectstack/service-analytics(可观测行为变更:一个此前被静默收窄读取的形状改为 400 拒收)。触达性(与 #6386 的一个实质差别)
undefined过不了 JSON,本形状过得了:它可以躺在库存 dashboardfilter/ reportruntimeFilter/ datasetfilter里,也可以由 AI 直接写出({amount: {gte: 10, $lte: 20}}正是漏$的典型手误)。examples 全扫无现存实例,issue 正文「未证实触达」维持成立;本 PR 把这个形状从「静默画一张比作者写的宽的图」变成一句点名键、给出两种改法的 400。Generated by Claude Code